require 'socket'
require 'classes'
require 'threads'
require 'serialisation'
require 'logging'

-- Network operations can only block for 5 milliseconds
-- Note that due to OS and Lua overheads in data transfer,
-- large chunks of data may block for longer
NetworkBlockTimeout = 0.005

NetworkCommands = {
    connect     = 0x01, -- client wants to connect
    disconnect  = 0x02, -- client is disconnecting
    ping        = 0x03, -- 'are you still there?'
    message     = 0x04, -- game message
}

NetworkResendTimeout = 1

table.rekey(NetworkCommands)

class ("NetworkObject", Object)

function NetworkObject:_construct(...)
    self.packetQueue = {}
    self:superClass()._construct(self, ...)
end

function NetworkObject:getInfo()
    if self.socket == nil then
        return nil
    end
    
    local info = {}
    
    sock = self.socket    
    info.address, info.port = sock:getsockname()
    --info.bytesReceived, info.bytesSent, info.socketAge = sock:getstats()
    
    return info
    
end

function NetworkObject:close()
    self.socket:close()
    self.socket = nil
end

function NetworkObject:sendDataToPeer(data, client)

    -- default to fast sending
    if (data.reliable == nil) then 
        data.reliable = false
    end
    
    if data.reliable then
        -- add this packet to the list of packets that need acknowledgement
        data.resendTimeout = NetworkResendTimeout
        
        -- generate a random 'packet id'
        
        local pid = math.floor(math.random()*10000000000)
        data.packetId = pid
        self.packetQueue[pid] = data
    end
    
    local serialisedData = serialise(data)
    
    if (client == nil) then
        self.socket:send(serialisedData)
    else
        self.socket:sendto(serialisedData, client.address, client.port)
    end
    
end

function NetworkObject:handleAcknowledgement(data)
    if (data.reliable) then
        
    end
end

--------------

class ("Server", NetworkObject)

function Server:_construct(...)
    self.socket = socket.udp()
    self:superClass():_construct(...)
    self.clients = {}
    self.log = Log:create("Server")
end

function Server:bind(address, port)
    -- reuse the port if it just got released
    -- apparently on w32 this actually acquires the port even if it's in use
    -- do we need to do something about this in that case?
    self.socket:setoption('reuseaddr', true)
    
    local success, message = self.socket:setsockname(address,port)
    if success == nil then
        error ("Could not create server: " .. message)
    end
    
end

function Server:startListening()
    
    self.listening = true
    self.connectedClients = 0
    
    -- make the socket only block for 5 milliseconds at a time
    self.socket:settimeout(NetworkBlockTimeout)
    
    self.log:print("Listening")
    
end

function Server:close()
    self:superClass().close(self) --oh wow can we think of a better way to do this please
    self.listening = false
end

function Server:clientDisconnected(oldClient)
    self.connectedClients = self.connectedClients - 1
    for i,client in ipairs(self.clients) do
        if client.address == oldClient.address and client.port == oldClient.port then
            table.remove(self.clients, i)
            break
        end
    end
    self:delegate("clientDisconnected", oldClient)
    
    self.log:print("Client at " .. oldClient.address .. " disconnected")
end

function Server:clientConnected(newClient)
    self.connectedClients = self.connectedClients + 1
    local newClient = {address=newClient.address, port=newClient.port}
    table.insert(self.clients, newClient)
    self:delegate("clientConnected", newClient)
    
    -- send back an acknowledgement packet
    local ack = {command=NetworkCommands.connect, acknowledge=true}
    self:sendDataToPeer(ack, newClient)
    
    self.log:print("Client at " .. newClient.address .. " connected")
end    

function Server:sendToAllClients(data)
    if self.clients == nil then return end
    for _,client in ipairs(self.clients) do
        self:sendDataToPeer(data, client)
    end
end

function Server:disconnectClient(client, reason)
    local disconnectNotification = {command=NetworkCommands.disconnect, reason=reason}
    self:sendDataToPeer(disconnectNotification, client)
    self:clientDisconnected(client)
end

function Server:messageReceived(data)
    self:delegate("messageReceived", data)
end

function Server:main()
    -- loop forever, dealing with client connections and receiving data
    while true do
        if self.listening then
            local data, address, port = self.socket:receivefrom()
            if data then
                data = deserialise(data)
                data.address, data.port = address, port
                
                if (data.acknowledgement) then
                    self:handleAcknowledgement(data)
                else
                
                    -- For each kind of message, we have a function
                    local cases = {
                        [NetworkCommands.connect] = self.clientConnected,
                        [NetworkCommands.disconnect] = self.clientDisconnected,
                        [NetworkCommands.message] = self.messageReceived
                    }
                    local case = cases[data.command]
                    if (case) then
                        case(self, data)
                    end
                    
                end
                
            end            
        end
        yield()
    end
end

---------

class ("Client", NetworkObject)

function Client:_construct(...)
    self.socket = socket.udp()
    self.connected = false
    self:superClass():_construct(...)
    self.log = Log:create("Client")
end

function Client:connect(address, port, connect_timeout)
    -- TODO: allow asynchronous connections
    
    -- default to a 2 second connect timeout
    --local connect_timeout = connect_timeout or 2 
    --self.socket:settimeout(connect_timeout)
    
    local connectRequest = {command=NetworkCommands.connect}
    
    self.socket:setpeername(address, port)
    self:sendDataToPeer(connectRequest)
    
    self.connected = false -- wait until acknowledgement before setting this to true
    
    self.socket:settimeout(NetworkBlockTimeout)
    self.log:print("Connecting to " .. address .. "...")
end

function Client:disconnect()
    local disconnectNotification = {command=NetworkCommands.disconnect}
    self:sendDataToPeer(disconnectNotification)
    self.connected = false
end

function Client:handleAcknowledgement(data)
    if data.command == NetworkCommands.connect then
        self:serverConnected()
    end
    self:superClass().handleAcknowledgement(self,data)
end

function Client:serverConnected(data)
    self.connected = true
    self.log:print("Connected to server")
end

function Client:serverDisconnected(data)
    if (data.reason) then
        -- TODO: better handling of this situation
        self.log:print("Got disconnected from server: " .. data.reason)
    else
        self.log:print("Disconnected from server (no reason given)")
    end
    self.connected = false
    self:delegate("serverDisconnected", data)
end

function Client:messageReceived(data)
    self:delegate("messageReceived", data)
end

function Client:main()
    while true do
        local data, message = self.socket:receive()
        if data then
            data = deserialise(data)
            
            if data.reliable then
                local ack = {
                    
                }
                self:sendDataToPeer()
            end
            
            if data.acknowledge == true then
                self:handleAcknowledgement(data)
            else
            
                self:delegate("networkDataReceived", data, self.socket:getpeername())
            
                -- For each kind of message, we have a function
                local cases = {
                    [NetworkCommands.disconnect] = self.serverDisconnected,
                    [NetworkCommands.message] = self.messageReceived
                }
                local case = cases[data.command]
                if (case) then
                    case(self, data)
                end
            end
            
        else
            -- timed out
        end
        yield()
    end
end

